<?php

namespace App\Plugins;

use \App\Acl\AclList,
    \App\Helpers\Utils,
    \App\Helpers\CryptHelper,
    \App\Helpers\LoggerHelper,
    \App\V2\Repositories\OAuth2Repository,
    Phalcon\Mvc\Dispatcher,
    Phalcon\Mvc\Dispatcher\Exception as DispatchException;

/**
 * 调度器相关插件类
 * @package App\Plugins;
 *
 * @property \App\Http\Request $request
 * @property \App\Http\Response $response
 * @property \App\Helpers\LoggerHelper $log
 */
class DispatchPlugin extends \Phalcon\Mvc\User\Plugin {

    #region 方法注释常量

    /**
     * 需要签名注释
     */
    const SIGN = "Sign";

    /**
     * 不需要签名注释
     */
    const UN_SIGN = "UnSign";

    /**
     * 需要登录注释
     */
    const AUTH = "Auth";

    /**
     * 不需要登录注释
     */
    const UN_AUTH = "UnAuth";

    /**
     * 关联登录注释
     */
    const RELATE_AUTH = "RelateAuth";

    /**
     * 允许的请求方式注释
     */
    const HTTP_METHOD = "HttpMethod";

    /**
     * 允许的请求方式参数名
     */
    const HTTP_METHOD_NAME = "name";

    /**
     * 参数验证说明注释
     */
    const PARAMETER = "Parameter";

    /**
     * 参数验证说明的参数名
     */
    const PARAMS = "params";

    #endregion

    #region 属性

    /**
     * 文件日志帮助类
     * @var \App\Helpers\LoggerHelper
     */
    protected $log;

    /**
     * 是否启用签名和加解密
     * @var bool
     */
    protected $enableSign;

    /**
     * 许可的请求方式
     * @var bool
     */
    protected $methods = FALSE;

    /**
     * 是否需要签名和加解密
     * @var bool
     */
    protected $needSign = TRUE;

    /**
     * 登录认证类型（0：不需要认证；1：需要认证；2：关联认证）
     */
    protected $authType = 0;

    /**
     * 请求参数规则
     * @var array
     */
    protected $rules = array();

    /**
     * 请求参数
     * @var array
     */
    protected $params = array();

    /**
     * HTTP请求环境对象
     * @var \App\Http\Request
     */
    protected $request;

    /**
     * HTTP响应环境对象
     * @var \App\Http\Response
     */
    protected $response;

    /**
     * RSA 相关配置信息
     * @var \Phalcon\Config
     */
    protected $rsaConfig;

    /**
     * 数据签名加密组件
     * @var \App\Helpers\RsaHelper
     */
    protected $crypt;

    #endregion

    /**
     * 调度器相关插件类构造函数.
     */
    public function __construct() {
        $config = Utils::configItem("app");
        $this->rsaConfig = Utils::configItem("rsa");
        $this->enableSign = isset($config->enableSign) && $config->enableSign;
    }

    #region 在执行控制器/动作方法前触发

    /**
     * 在执行控制器
     * @param $event
     * @param $dispatcher
     * @throws \Exception
     * @return boolean
     */
    public function beforeExecuteRoute($event, Dispatcher $dispatcher) {

        $this->request = $this->di->getShared("request");
        $this->response = $this->di->getShared("response");
        // 记录日志开始
        LoggerHelper::begin();

        // 获取控制器类和操作方法的注释
        $this->_analysisAnnotations($dispatcher);

        //分析Http请求方式是否匹配
        $state = $this->_analysisHttpMethod();

        //分析参数的签名是否正确
        if ($state === 200) {
            $state = $this->_analysisParam();
        }

        // 认证登录信息
        if ($state === 200) {
            $state = $this->_authentication();
        }

        // 访问控制
        if ($state === 200) {
            $state = $this->_accessControl();
        }

        // 提交日志
        LoggerHelper::commit();

        if ($state == 200 && $this->_verifyParams()) {
            return TRUE;
        } elseif ($state != 200) {
            $this->response->sendResponse($state, $state);
        }
        return FALSE;
    }

    #endregion

    #region 在执行控制器/动作方法后触发

    /**
     * 在执行控制器/动作方法后触发
     * @param $event
     * @param Dispatcher $dispatcher
     */
    public function afterDispatchLoop($event, Dispatcher $dispatcher) {
        //处理响应内容
        $this->_handlerResponse();
        $dispatcher->setReturnedValue($this->response);
        //$this->response->send();
    }

    #endregion

    #region 处理请求错误 （4字头） 和 500错误 错误

    /**
     * 在调度器抛出任意异常前触发
     * @param Dispatcher $dispatcher
     * @param \Exception $exception
     * @return bool
     */
    public function beforeException($event, Dispatcher $dispatcher, \Exception $exception) {

        $this->response = $this->di->getShared("response");
        try {
            LoggerHelper::begin();
            LoggerHelper::error("请求出错");
            LoggerHelper::error("错误信息：{$exception->getMessage()}");
            LoggerHelper::error("错误堆栈信息：{$exception->getTraceAsString()}");
            LoggerHelper::commit();

            if ($exception instanceof DispatchException) {
                //处理请求错误（4字头）
                $this->response->sendResponse(404, 404);
            } else {
                //处理其他异常
                $this->response->sendResponse(500, 500);
            }
            //处理响应内容
            $this->_handlerResponse();
            $dispatcher->setReturnedValue($this->response);

        } catch (\Exception $e) {
            LoggerHelper::rollback();
        }
    }

    #endregion

    #region 在执行控制器/动作方法前触发的事件处理

    #region 分析控制器类和操作方法的注释

    /**
     * 分析控制器类和操作方法的注释
     * @param Dispatcher $dispatcher
     */
    private function _analysisAnnotations(Dispatcher $dispatcher) {
        /**
         * @var \Phalcon\Annotations\Adapter $reader 缓存适配器对象
         * @var \Phalcon\Annotations\Annotation $annotation 注释对象
         */

        //已解析和已处理的注释将永久保存在缓存中提升性能。
        $reader = $this->di->getShared("annAdapter");

        //控制器名
        $className = $dispatcher->getControllerClass();
        //方法名
        $method = $dispatcher->getActiveMethod();

        //解析类中所有的注释中发现一个类
        $reflection = $reader->get($className);

        //读取类中注释块中的注释
        $classAnnotations = $reflection->getClassAnnotations();
        //在指定的方法中获取注释集合
        $methodAnnotations = $reader->getMethod($className, $method);

        //分析控制器类注释
        if ($classAnnotations) {
            // 如果包含“Sign” 则表示整个控制器都需要加解密签名处理
            $this->needSign = $classAnnotations->has(self::SIGN);
            // 如果包含“Auth”则表示，整个控制器的操作都需要进行登录安全认证，默认为需要进行登录安全认证
            if ($classAnnotations->has(self::AUTH) || !$classAnnotations->has(self::UN_AUTH)) {
                $this->authType = 1; // 需要认证
            } elseif ($classAnnotations->has(self::UN_AUTH)) {
                $this->authType = 0; // 不需要认证
            } elseif ($classAnnotations->has(self::RELATE_AUTH)) {
                $this->authType = 2; // 关联认证
            }
        }

        //分析方法注释
        if ($methodAnnotations) {
            // 如果整个控制器需要加解密签名处理，那么当前方法则需要声明“UnSign”表示该方法不需要加解密签名处理，默认为需要；
            // 如果整个控制器不需要加解密处理，那么当前方法则需要声明“Sign”表示该方法需要加解密签名处理，默认为不需要。
            $this->needSign = $this->needSign ? !$methodAnnotations->has(self::UN_SIGN) : $methodAnnotations->has(self::SIGN);

            // 如果整个控制器需要进行登录安全认证，那么当前方法则需要声明“UnAuth”表示该方法不需要进行登录安全认证，默认为需要；
            // 如果整个控制器不需要进行登录安全认证，那么当前方法则需要声明“Auth”表示该方法需要进行登录安全认证，默认为不需要。
            if ($methodAnnotations->has(self::AUTH)) {
                $this->authType = 1; // 需要认证
            } elseif ($methodAnnotations->has(self::UN_AUTH)) {
                $this->authType = 0; // 不需要认证
            } elseif ($methodAnnotations->has(self::RELATE_AUTH)) {
                $this->authType = 2; // 关联认证
            }

            // 获取允许的请求方式
            if ($methodAnnotations->has(self::HTTP_METHOD)) {
                //获取HttpMethod注释对象
                $annotation = $methodAnnotations->get(self::HTTP_METHOD);
                //获取参数name的值
                $this->methods = $annotation->getArgument(self::HTTP_METHOD_NAME);
            }

            // 获取参数
            if ($methodAnnotations->has(self::PARAMETER)) {
                //获取HttpMethod注释对象
                $annotation = $methodAnnotations->get(self::PARAMETER);
                //获取参数name的值
                $this->rules = $annotation->getArgument(self::PARAMS);
            }
        }
    }

    #endregion

    #region 分析Http请求方式是否匹配

    /**
     * 分析Http请求方式是否匹配
     */
    private function _analysisHttpMethod() {
        //当前的请求方式
        $currentMethod = strtoupper($this->request->getMethod());
        if ($this->methods) {
            $isMatch = is_array($this->methods) ? in_array($currentMethod, $this->methods) : $currentMethod == strtoupper($this->methods);
        } else {
            $isMatch = $currentMethod == "GET";
        }
        if (!$isMatch) {
            //资源不允许的请求方式 返回405
            return 405;
        }
        return 200;
    }

    #endregion

    #region 分析请求参数

    /**
     * 分析请求参数
     */
    private function _analysisParam() {
        // 获取QueryString的请求参数
        $queryString = $this->request->getQueryString();
        //获取POST的请求参数
        $post = $this->request->getPost();

        //根据配置判断是否要进行验签，分析参数签名是否正确
        LoggerHelper::debug("是否需要签名:" . ($this->needSign ? "需要" : "不需要"));
        if ($this->enableSign && $this->needSign) {
            // 获取数据签名加密组件
            $this->crypt = $this->di->getShared("crypt");
            // 验证QueryString
            if (!empty($queryString)) {
                $isValid = $this->_handlerParam($queryString);
                LoggerHelper::debug("解析后的参数:" . json_encode($queryString, JSON_UNESCAPED_UNICODE));

                if ($isValid)
                    $this->request->append($queryString);
                else
                    return 400;
            }
            // 验证POST请求参数
            if (!empty($post)) {
                $isValid = $this->_handlerParam($post);
                if ($isValid)
                    $this->request->append($post);
                else
                    return 400;
            }
        } else {
            //组装参数
            if (!empty($post))
                $this->request->append($post);

            if (!empty($queryString))
                $this->request->append($queryString);
        }
        // 所有请求参数
        $this->params = $this->request->getRequest();
        LoggerHelper::debug("解析后的参数:" . json_encode($this->params, JSON_UNESCAPED_UNICODE));
        return 200;
    }

    #endregion

    #region 验证请求参数

    /**
     * 验证请求参数
     */
    private function _verifyParams() {
        if ($this->rules) {
            foreach ($this->rules as $key => $param) {
                $name = $param["name"];
                $rule = $param["rule"];
                if ($rule) {
                    $validationTypes = explode("|", $rule);
                    foreach ($validationTypes as $type) {
                        $message = $this->_verifyParam($key, $type, $name);
                        if ($message !== true) {
                            $this->response->sendResponseContent(array(
                                "status_code" => 10001,
                                "message" => $message), 200);
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * 验证请求参数
     * @param $key 请求参数Key
     * @param $type 验证类型
     * @param $name 请求参数名称
     * @return boolean
     */
    private function _verifyParam($key, $type, $name) {
        $value = isset($this->params[$key]) ? $this->params[$key] : "";
        static $validation;
        if (!isset($validation))
            $validation = new \App\Helpers\ValidationHelper();

        $rules = explode("=", $type);
        if (count($rules) == 1) {
            if (!$validation->$type($value)) {
                return $validation->getErrorMessage($type, $name);
            }
        } else {
            $type = $rules[0];
            $params = explode(",", $rules[1]);
            if (count($params) == 1 || $type == "in") {
                if (!$validation->$type($value, count($params) == 1 ? $params[0] : $params)) {
                    return $validation->getErrorMessage($type, $name, $type == "in" ? null : $params);
                }
            } elseif ($type == "relate") {
                $relateKey = $params[0];
                $list = explode("&", $params[1]);
                $relateType = $params[2];
                $relateValue = isset($this->params[$relateKey]) ? $this->params[$relateKey] : "";
                $ret = $validation->in($relateValue, $list);
                if ($ret) {
                    return $this->_verifyParam($key, $relateType, $name);
                }
            }
        }
        return true;
    }

    #endregion

    #region 认证登录信息

    /**
     * 认证登录信息
     * @return int 登录授权未通过时，返回401；正常时，返回200
     */
    private function _authentication() {

        /**
         * @var $userId 用户编号
         * @var $token 登录Token信息
         */

        $state = 200;
        // 必须认证时，请求头信息必须提交
        if ($this->authType) {
            $headers = Utils::getAllHeaders();
            if (isset($headers["Authorization"]) && $headers["Authorization"]) {
                //Http请求头 Basic认证信息
                $authorization = explode(" ", $headers["Authorization"]);
                if (count($authorization) == 2) {
                    $tokenType = $authorization[0];
                    $accessToken = $authorization[1];
                    $repository = new OAuth2Repository($this->di);
                    // 进行登录认证
                    $userInfo = $repository->auth($tokenType, $accessToken);
                    \App\Helpers\LoggerHelper::debug("登录用户Token : {$tokenType} {$accessToken}");
                    \App\Helpers\LoggerHelper::debug("登录用户信息 : " . json_encode($userInfo));
                    if ($userInfo) {
                        $this->request->setUser($userInfo);
                    } else {
                        $state = 401;
                    }
                } else {
                    $state = 401;
                }
            } elseif ($this->authType == 1) {
                $state = 401;
            }
        }
        return $state;
    }

    #endregion

    #region 访问控制

    /**
     * 访问控制
     */
    private function _accessControl() {

        if (BUILD_VERSION == BUILD_LOCAL) {
            return 200;
        }

        // 访问控制列表管理器
        $acl = AclList::init($this->di);

        //当前控制器名称
        $controller = $this->dispatcher->getControllerName();
        //当前控制器名称
        $action = $this->dispatcher->getActionName();

        // 获取用户权限组
        $roles = $this->request->getUser("roles");
        if (empty($roles)) {
            // 普通用户
            $roleName = AclList::GENERAL;
            // 检查角色是否有访问权限
            $result = $acl->isAllowed($roleName, $controller, $action);
        } else {
            foreach ($roles as $roleName) {
                $result = $acl->isAllowed($roleName, $controller, $action);
                if ($result) {
                    break;
                }
            }
        }
        return $result ? 200 : 402;
    }

    #endregion

    #endregion

    #region 在执行控制器/动作方法后触发的事件处理

    /**
     * 处理响应内容
     */
    private function _handlerResponse() {
        // 针对响应内容进行加密签名操作
        if ($this->enableSign && $this->needSign) {
            if (!isset($this->crypt)) {
                $this->crypt = $this->di->getShared("crypt");
            }
            // 获取响应内容
            $content = $this->response->getContent();
            LoggerHelper::debug("处理响应内容：" . json_encode($content));
            // 加密后的字符串
            $encrypt = $this->crypt->encrypt($content, $this->rsaConfig->request->pubKey);
            // 数字签名
            $sign = $this->_rsaSign($encrypt, "response");
            $this->response->setJsonContent(array(
                "encrypt" => $encrypt,
                "sign" => $sign
            ));
        }
    }

    #endregion

    #region 验证签名、解密

    /**
     * 验证签名，解密参数
     * @param $params 要处理的参数
     * @return boolean
     */
    private function _handlerParam(&$params) {

        /**
         * @var \App\Helpers\CryptHelper $cryptHelper 加解密帮助对象
         */
        LoggerHelper::debug("解析前的请求参数：" . json_encode($params));

        if (isset($params["sign"]) && isset($params["encrypt"])) {
            // 签名字符串
            $sign = $params["sign"];
            // 加密字符串
            $encrypt = $params["encrypt"];
            if ($this->_rsaVerify($encrypt, $sign)) {
                // 解密字符串
                $decrypt = $this->crypt->decrypt($encrypt, $this->rsaConfig->response->priKey);

                if (strpos($decrypt, "{") === 0) {
                    $params = json_decode($decrypt, true);
                } else {
                    // 解析查询参数
                    $params = $this->request->analysisQueryString($decrypt);
                }
                LoggerHelper::debug("解析后的请求参数：" . json_encode($params));
                return TRUE;
            }
        }
        return FALSE;
    }

    /**
     * 验证签名
     * @param $data 要验证的字符串
     * @param $sign 签名的字符串
     * @param string $rsaKey RSA签名配置key
     * @return boolean
     */
    private function _rsaVerify($data, $sign, $rsaKey = "request") {
        //rsa签名配置
        $pubKey = $this->rsaConfig->$rsaKey->pubKey;
        return $this->crypt->verify($data, $sign, $pubKey);
    }

    /**
     * 添加RSA签名
     * @param $data 需要签名的字符串
     * @return string RSA签名字符串
     * @param string $rsaKey RSA签名配置key
     */
    protected function _rsaSign($data, $rsaKey = "request") {
        //rsa签名配置
        $priKey = $this->rsaConfig->$rsaKey->priKey;
        return $this->crypt->sign($data, $priKey);
    }

    #endregion

}